#!/usr/bin/env ruby
require 'openssl'

class X509Collector
  include Enumerable

  def initialize
    @collected_data = {}
  end

  def interpret_contents(contents)
    cls = case contents.split("\n")[0]
          when /BEGIN X509 CRL/ then OpenSSL::X509::CRL
          when /BEGIN CERTIFICATE REQUEST/ then OpenSSL::X509::Request
          when /BEGIN CERTIFICATE/ then OpenSSL::X509::Certificate
          when /BEGIN RSA (PRIVATE|PUBLIC) KEY/ then OpenSSL::PKey::RSA
          else return nil
          end
    cls.new(contents)
  rescue
    nil
  end

  def expected_non_x509_files
    ['inventory.txt', 'ca.pass', 'serial']
  end

  def investigate_path(path)
    if File.directory?(path)
      Dir.foreach(path) do |x|
        next if ['.', '..'].include? x
        investigate_path File.join(path, x)
      end
    else
      contents = File.read path
      meaning = interpret_contents contents
      unless meaning || expected_non_x509_files.include?(File.basename(path))
        puts "WARNING: file #{path.inspect} could not be interpreted"
      end
      @collected_data[path] = meaning if meaning
    end
  end

  def each(&block)
    @collected_data.each(&block)
  end

  def extract_public_key_info(path, meaning)
    case meaning
    when OpenSSL::PKey::RSA
      if meaning.private?
        [meaning.public_key, 2, path]
      else
        [meaning, 3, path]
      end
    when OpenSSL::X509::Certificate
      [meaning.public_key, 0, meaning.subject.to_s]
    when OpenSSL::X509::Request
      [meaning.public_key, 1, meaning.subject.to_s]
    end
  end

  def who_signed(meaning, key_names, keys)
    signing_key = keys.find { |key| meaning.verify(key) }
    if signing_key then "#{key_names[signing_key.to_s]}" else "???" end
  end

  def explain(meaning, key_names, keys)
    case meaning
    when OpenSSL::PKey::RSA
      if meaning.private?
        "Private key for #{key_names[meaning.public_key.to_s]}"
      else
        "Public key for #{key_names[meaning.public_key.to_s]}"
      end
    when OpenSSL::X509::Certificate
      signature_desc = who_signed(meaning, key_names, keys)
      "Certificate assigning name #{meaning.subject.to_s} to #{key_names[meaning.public_key.to_s]}\n    serial number #{meaning.serial}\n    issued by #{meaning.issuer.to_s}\n    signed by #{signature_desc}"
    when OpenSSL::X509::Request
      signature_desc = who_signed(meaning, key_names, keys)
      "Certificate request for #{meaning.subject.to_s} having key #{key_names[meaning.public_key.to_s]}\n    signed by #{signature_desc}"
    when OpenSSL::X509::CRL
      signature_desc = who_signed(meaning, key_names, keys)
      revoked_serial_numbers = meaning.revoked.map { |r| r.serial }
      revoked_desc = if revoked_serial_numbers.count > 0 then "serial numbers #{revoked_serial_numbers.inspect}" else "nothing" end
      "Certificate revocation list revoking #{revoked_desc}\n    issued by #{meaning.issuer.to_s}\n    signed by #{signature_desc}"
    else
      "Unknown"
    end
  end

  # Yield unique public keys, with a canonical name for each.
  def collect_public_keys
    key_data = {} # pem => (priority, name, public_key)
    @collected_data.collect do |path, meaning|
      begin
        next unless public_key_info = extract_public_key_info(path, meaning)
        public_key, priority, name = public_key_info
        pem = public_key.to_s
        existing_priority, existing_name, existing_public_key = key_data[pem]
        next if existing_priority and existing_priority < priority
        key_data[pem] = priority, name, public_key
      rescue
        puts "exception!"
      end
    end
    name_to_key_hash = {}
    key_data.each do |pem, data|
      priority, name, public_key = data
      if name_to_key_hash[name]
        suffix_num = 2
        while name_to_key_hash[name + " (#{suffix_num})"]
          suffix_num += 1
        end
        name = name + " (#{suffix_num})"
      end
      name_to_key_hash[name] = public_key
    end
    key_names = {}
    keys = []
    name_to_key_hash.each do |name, public_key|
      key_names[public_key.to_s] = "key<#{name}>"
      keys << public_key
    end
    [key_names, keys]
  end
end

collector = X509Collector.new
ARGV.each do |path|
  collector.investigate_path(path)
end
key_names, keys = collector.collect_public_keys
collector.map do |path, meaning|
  [collector.explain(meaning, key_names, keys), path]
end.sort.each do |description, path|
  puts "#{path}:"
  puts "  #{description}"
  puts
end
